Skip to content

feat: full §10.2 HDKD agent bootstrap — broker link-code endpoints + daemon redeem (#144)#149

Merged
hanwencheng merged 23 commits into
mainfrom
claude/impl-144-hdkd-bootstrap
May 31, 2026
Merged

feat: full §10.2 HDKD agent bootstrap — broker link-code endpoints + daemon redeem (#144)#149
hanwencheng merged 23 commits into
mainfrom
claude/impl-144-hdkd-bootstrap

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

Issue #144 — Full arch.md §10.2 agent-bootstrap (HDKD omni + broker link-code endpoints)

Closes #144. Converges the PR #141 interim §10.2 (agent omni derived from the agent's own wallet; openssl rand link-code stub) to the literal ceremony.

The flow (the "install an app → approve its permissions" story)

  1. P.0 create (master) — agentkeys agent create --label agent-a --services memory → broker mints a one-time link code bound to the HDKD child omni O_agent = SHA256("agentkeys-hdkd-v1" ‖ O_master ‖ "//label").
  2. P.1 install (agent, in the sandbox) — agentkeys-daemon --init-link-code <code> generates its own K10 device key (never on the master), proves possession, redeemsJ1_agent. The broker records a pending binding.
  3. P.2 bind (master) — submits registerAgentDevice (no biometric).
  4. P.3 grant (master) — setScopeWithWebauthn (one Touch ID). P.2+P.3 are conceptually one approval; two steps for deterministic test automation.

Decisions (asked + answered)

  1. Master submits the on-chain binding — broker mints the code + J1_agent + records the pending binding; the master pulls it and binds. No Heima-mainnet contract change, no broker chain key. Async/push model (master = phone).
  2. Child omni is PUBLIC + recomputable — unforgeability = the J1_master-gated /v1/agent/create + the master-submitted binding, NOT a secret. Agent keeps a K10 device key only (omni decoupled).
  3. Daemon owns keygen + redeem (--init-link-code), sharing agentkeys-core::device_crypto with the CLI.

What landed

  • core: device_crypto (shared K10 keygen / EIP-191 / ecrecover / pop_sig + DeviceKey) + HDKD child_omni/child_omni_hex + validate_label (frozen vectors).
  • broker: POST /v1/agent/create, POST /v1/auth/link-code/redeem (pop_sig verified before consume → retryable), GET /v1/agent/pending-bindings; SQLite link-code + pending-binding store; AgentKeysClaims + mint_agent_session_jwt; mint-oidc-jwt reads actor_omni from the verified claim (STS-relay prerequisite; wallet sessions byte-identical, regression-tested).
  • daemon: --init-link-code one-shot.
  • cli: agent create + agent pending (master-side).
  • harness: Phase P rework (P.0→P.3); builds + uploads the daemon binary.
  • ci / broker-setup: §10.2 route smoke (401 = live, 404 = stale binary) + nm symbol check in setup-broker-host.sh; bumped the deploy job timeout 15→25min + SSM executionTimeout 900→1500 for the larger broker build closure (agentkeys-core pulls aws-sdk-s3/keyring/aes-gcm).
  • docs: arch.md §10.2 / §5 / §6.2 / route list; runbook Phase P + troubleshooting; docs/spec/plans/issue-144-hdkd-agent-bootstrap.md.

Deviation (vs the asked plan)

CLI agent bind/agent grant Rust subcommands are not added — chain submission lives in shell + cast, and the two existing chain helpers (heima-agent-create.sh --from-pubkey = bind, heima-scope-set.sh --webauthn = grant) already are the deterministic two-step split. The CLI ships the genuinely-new master surfaces instead (create + pending, incl. the production rendezvous). Recorded in the plan doc.

Out of scope (deferred)

Broker chain-write / meta-tx; secret-keyed HDKD; HDKD sub-actors; broker-side K11 verify (stays on-chain); production APNs/FCM push transport (the pending-binding data model + endpoint ship now).

Tests

  • cargo test -p agentkeys-core — 141 (HDKD frozen vectors, pop_sig sign→ecrecover round-trip).
  • cargo test -p agentkeys-broker-server --features auth-email-link — 179 lib + agent_bootstrap_flow (create-gated, bad-label, full create→redeem→pending, bad-pop_sig-retryable) + oidc byte-identical regression.
  • cargo clippy --workspace --all-targets -- -D warnings (default features) — clean. cargo fmt --all --check — clean. bash -n harness/phase1-wire-demo.sh + scripts/setup-broker-host.sh — OK.
  • The full --real --webauthn end-to-end needs the redeployed broker + Touch ID + a live sandbox — exercised after this deploy (CI deploy + the route smoke confirm the §10.2 code is live on the test broker).

🤖 Generated with Claude Code

…daemon redeem (#144)

Converges the PR #141 interim §10.2 to the literal ceremony: the master mints a
one-time link code bound to a hard-derived child omni
O_agent = SHA256("agentkeys-hdkd-v1" || O_master || "//label"); the agent daemon
generates its own K10 in the sandbox, redeems the code (pop_sig), and the broker
mints J1_agent carrying the HDKD omni + parent lineage. The master then approves
the binding + scope async (push → one Touch ID), iOS/Android-style.

- core: device_crypto (shared K10 keygen / EIP-191 / ecrecover / pop_sig + DeviceKey)
  + HDKD child_omni/child_omni_hex + validate_label (frozen vectors)
- broker: POST /v1/agent/create (J1_master-gated), POST /v1/auth/link-code/redeem
  (pop_sig verified before consume → retryable), GET /v1/agent/pending-bindings;
  SQLite link-code + pending-binding store; AgentKeysClaims + mint_agent_session_jwt;
  mint-oidc-jwt now reads actor_omni from the verified claim (STS-relay prerequisite;
  wallet sessions byte-identical, regression-tested)
- daemon: --init-link-code one-shot (in-sandbox keygen → redeem → persist J1_agent
  → emit binding artifact)
- cli: agentkeys agent create + agent pending (master-side)
- harness: Phase P rework — P.0 create (real broker code) → P.1 install (daemon
  --init-link-code) → P.2 bind → P.3 grant; build + upload the daemon binary
- ci / broker-setup: §10.2 route smoke (401 = live, 404 = stale binary) + nm symbol
  check in setup-broker-host.sh; bump the deploy job timeout 15→25min + SSM
  executionTimeout 900→1500 for the larger broker build closure (agentkeys-core)
- docs: arch.md §10.2 (async master-submits ceremony), §5 agent_omni row, §6.2,
  route list; operator-runbook-wire.md Phase P + troubleshooting; new
  docs/spec/plans/issue-144-hdkd-agent-bootstrap.md

Decisions (master submits the on-chain binding — no contract change; child omni is
public + recomputable; daemon owns keygen+redeem with shared core) and deviations
are recorded in docs/spec/plans/issue-144-hdkd-agent-bootstrap.md.

Tests: core 141; broker 179 lib + agent_bootstrap_flow integration + oidc regression;
clippy -D warnings clean (default features); cargo fmt clean; harness bash -n OK.
…rade prereq + P.0 in the install step

The Phase P rewrite landed in the prior commit; this closes the two gaps an
operator would hit: (1) a Real-mode prereq that the broker must be running the
issue-144 code (else Phase P P.0 agent create 404s — folds in the
setup-broker-host.sh --upgrade fix + the 401-not-404 deploy self-check), and
(2) P.0 (master mints the link code) in the install-step summary bullet.
…us (idempotent)

- demo (harness Phase P): P.0 now drives the agentkeys agent create CLI (was raw
  curl); P.1b drives agentkeys agent pending (the master-pull rendezvous); P.2
  acks the broker after binding so pending self-cleans. Falls back to a raw POST
  only when no local host binary exists.
- broker: new POST /v1/agent/pending-bindings/ack (J1_master-gated, operator-scoped
  mark_bound). The rendezvous never cleared before (bound_at was never set), so
  pending listed redeemed agents forever; the ack fixes that AND makes the list
  idempotent. agent_bootstrap_flow now asserts pending=1 then ack then pending=0.
- idempotency: AGENT_LABEL stays stable (deterministic HDKD omni) and the K10 file
  persists in the long-lived sandbox (clean_slate never wipes ~/.agentkeys), so
  registerAgentDevice hits already-registered, scope re-set + seed overwrite are
  no-ops, and the ack keeps pending clean — re-runs converge.
- docs: arch.md route list + 10.2 ack step; runbook Phase P (P.1b + ack + stable-label note).

broker 179 lib + agent_bootstrap_flow (incl. ack) green; clippy -D warnings clean.
… references

Per the CLAUDE.md never-pass---upgrade rule (it is a back-compat no-op; the
script is idempotent):
- operator-runbook-wire.md broker-version prereq: --ref main instead of --upgrade.
- setup-broker-host.sh: idempotency comment no longer illustrates with --upgrade.
…test brokers)

setup-cloud.sh step 15 (SSM SendCommand to bring up the MCP server) assumed the
broker EC2 was already a registered SSM managed instance but never ensured it.
Operators hit `SendCommand -> InvalidInstanceId` because the broker-host role was
created WITHOUT AmazonSSMManagedInstanceCore, so the on-host amazon-ssm-agent
can't register. (And separately, a caller lacking ssm:SendCommand got a
misleading "does the instance have the agent?" message.)

- ensure_ssm_managed(): runs before SendCommand. Resolves the role from the
  INSTANCE's attached profile (naming-agnostic, so the SAME code fixes BOTH the
  prod `agentkeys-broker-host` and the test broker's own profile), idempotently
  attaches AmazonSSMManagedInstanceCore if missing, then polls
  describe-instance-information until PingStatus=Online. If the agent never
  registers (role now correct => the agent itself isn't running), it dies with
  the exact restart remediation (ssh-broker.sh + setup-broker-host.sh --upgrade,
  or reboot). Idempotent: a re-run with the policy already attached skips.
- SendCommand now captures stderr and distinguishes a CALLER ssm:SendCommand
  AccessDenied (identity-based policy gap) from a real instance problem, with a
  precise remediation (put-user-policy; see provision-ci-deploy-role.sh for the
  policy shape) instead of the misleading instance-agent message.
- aws iam calls are global (no --region); ec2/ssm reads pass --region "$REGION"
  per the agentkeys-admin-defaults-to-us-west-2 trap (CLAUDE.md).

Env-agnostic + idempotent: works for both broker envs and converges on re-run.
… refs + add CLAUDE.md rule

setup-broker-host.sh treats --upgrade (and --skip-pull) as back-compat NO-OPS
(it is idempotent + auto-detects bootstrap vs upgrade), so emitting it is
misleading. Replace active-path references with --ref main (the canonical
idempotent deploy invocation per CLAUDE.md) and codify the rule:
- setup-cloud.sh: ensure_ssm_managed remediation suggests --ref main.
- docs/ci-setup.md: prod-broker manual deploy uses --ref main.
- CLAUDE.md (Remote broker host): explicit never-pass---upgrade rule.
…-u unbound-var)

SSM RunShellScript executes the step-15 mcp-bring-up script as root with a
MINIMAL env (no HOME). Under set -euo pipefail the first $HOME use
(export PATH=$HOME/.cargo/bin) aborted with 'HOME: unbound variable' — a latent
bug that only surfaced once SSM delivery started working (previously it failed at
send-command). Default HOME to /root before any $HOME use. Also document
--only-step 15 in the script's re-run examples.
…etup script

Broaden the rule from setup-broker-host.sh to all idempotent setup scripts
(setup-cloud.sh, setup-heima.sh, heima-* helpers) and restore the actionable
guidance (invoke plain / --only-step N, or --ref main for a broker redeploy;
replace existing active-path --upgrade references).
…ld is not a hang)

setup-mcp-host.sh runs 'cargo install --git' to build agentkeys-mcp-server FROM
SOURCE; a cold build on the t3.medium broker takes 10-20 min, but step 15 polled
SILENTLY with a 10-min cap — so it looked hung and timed out mid-build. Add a
~30s heartbeat (status + elapsed) and raise the cap to 25 min. On timeout, state
the build is likely still running on the broker (not a failure) and point to
--from-step 15 to resume (cargo cache => fast) plus the get-command-invocation
watch command.
…argo build, not cargo install --git

Step 15 took ~10 min because setup-mcp-host.sh used 'cargo install --git --force':
it re-clones the repo and builds agentkeys-mcp-server + its WHOLE dep tree
(aws-sdk-s3, tokio, k256, reqwest, ...) in a THROWAWAY target every run — a cold
release build on the 2-vCPU t3.medium broker. setup-broker-host.sh already
compiled those same deps into $REPO_ROOT/target/release (persistent, incremental),
and the harness builds the MCP server with persistent cargo caches too; the
cargo-install path was the lone build throwing the cache away.

Build it the same way the broker/workers (and harness) do: cd $REPO_ROOT
(/opt/agentkeys-src, already checked out by step 15's clone/reset) and
'cargo build --release --locked -p agentkeys-mcp-server', install from
target/release. Reuses the shared dep cache => incremental (seconds-2 min), not a
10-20 min cold build. Worst case (no prior build) is no slower than before, and
all re-runs are fast since target/ persists.
…up, drop operator flags

- harness/phase1-wire-demo.sh: cross-build target -> named docker volume (off the macOS bind-mount); copy only the 3 binaries out to the host path the upload step reads.

- setup-cloud.sh: step 15 (hosted MCP on broker) is now a no-op redirect. It is a broker-host concern + the deferred Hosted-LLM path (#152), not cloud/IAM.

- setup-broker-host.sh / setup-mcp-host.sh: state-driven idempotency over operator flags. Drop --without-workers / --without-build / --clean / --no-clean; unattended by default (--yes/--non-interactive kept as accepted no-ops for CI). Hosted MCP auto-converges when its binary is already installed.

- docs/operator-runbook-wire.md: wrap-up section -- three setup entry points (run only what changed), no-flag idempotent posture, right test order; build-cache + troubleshooting updates.

- docs/spec/plans/issue-107-mcp-demo-runbook.md: correct the B.5 deploy path (setup-mcp-host.sh, no --with-mcp).

Refs #149, #152.
…-broker-host.sh step

operator-runbook-wire.md now tells a from-nothing operator exactly what to run and ON WHICH MACHINE: (1) setup-cloud.sh on the laptop, (2) ssh-broker.sh + sudo setup-broker-host.sh --ref <branch> ON the broker host, (3) setup-heima.sh, (4) the harness. Explains the --ref branch choice (claude/impl-144-hdkd-bootstrap until #149 merges) and why a 'git pull' on main shows nothing (the work is on the feature branch). Keeps the re-run-only-what-changed table.

Refs #149.
…registerAgentDevice

Before: AGENT_LABEL is stable + the sandbox K10 persisted, so P.2 hit the registerAgentDevice already-registered skip and the pairing path was never exercised ('ok ... or already-active'). The contract refuses to re-register a hash (registeredAt stays set even after revoke), so a real re-pair needs a NEW key.

Now, in fresh_pairing mode (--real without --reuse-agent): P.depair revokes the prior device (recorded as a PUBLIC-hash sidecar in the sandbox — key never leaves; heima-device-revoke.sh self-checks isActive + is idempotent, agent-tier so no Touch ID), wipes the sandbox K10 so P.1 mints a fresh key, and P.2 does a REAL registerAgentDevice. P.1 re-stashes the new hash for the next run's depair.

First run after this lands wipes the pre-existing ('hardcoded') key and registers fresh; that prior device stays an orphan (revoke it once by hand if you want it gone). Runbook tx-count notes updated (now ~2 txs/run: revoke + register).

Refs #149.
…-device vs stable-omni

The depair/re-pair was only in the TL;DR. Add P.depair to the 'What happens, in order' walkthrough, the Install-(pair) flag note, and the Phase-P troubleshooting row (which wrongly said 'the agent reuses its K10' — it now mints a fresh K10 each run).

Also reconcile the contradiction the depair surfaced: the label-derived omni is STABLE (same agent), only the device key is fresh — so memory persists and step 1.5 overwrites each run. Fixed the 'fresh/new agent identity ⇒ empty memory' wording in 4 spots.

Refs #149.
…f a bare 'no link code'

P.0 hitting the §10.2 routes on a stale broker returned a confusing 'agent/create returned no link code: HTTP 404'. Detect the 404 and name the exact fix (setup-broker-host.sh --ref claude/impl-144-hdkd-bootstrap on the broker) + the 401-not-404 verify. Notes that 1.4/1.5 are cascades. Refs #149.
… + root-owned fix

Operator hit 'unable to unlink … Permission denied' on a manual git pull because a prior 'sudo bash setup-broker-host.sh' ran git/cargo as root → root-owned repo. Note: use --ref (its git runs consistently), chown -R agentkey to restore manual git, and that the broker uses git not jj. Refs #149.
…git-pull Permission denied)

Running 'sudo bash setup-broker-host.sh' executes the script's plain git (--ref) + cargo as root, leaving root-owned files in the checkout that block the operator's next 'git pull' (error: unable to unlink … Permission denied). New §8c: when SUDO_USER is set, chown -R the repo back to the invoking user at the end — idempotent, scoped to $REPO_ROOT only (system files stay root/agentkeys-owned), no-op when run as the user directly or as root with no SUDO_USER (SSM/root-managed /opt clone). Runbook note updated. Refs #149.
…h registration (Codex finding #3)

P.depair swallowed revoke + K10-wipe failures with '|| true' then unconditionally reported 'clean slate', and P.2 grepped only '"ok":true' — which heima-agent-create.sh also returns for the already-registered SKIP (no tx_hash). So a stale-sidecar / unreachable-sandbox / failed-revoke run could greenlight 'a real registration' while only exercising the already-active path.

Now: (1) P.depair hard-fails if revoke is not ok:true, and confirms the sandbox K10 is actually gone (test -e) before claiming a clean slate; (2) P.1 hard-fails if the new device_key_hash == the prior sidecar hash (wipe didn't take); (3) P.2 distinguishes skipped:already-registered (FAIL — not a real registration) from a real tx_hash (ok) before acking. Refs #149.
…device-active (Codex finding #1)

mint_oidc_jwt accepted ANY valid session and signed an AssumeRoleWithWebIdentity JWT with no binding/scope check. Since /v1/auth/link-code/redeem mints J1_agent pre-binding, a redeemed-but-unapproved agent could get STS creds to its own actor prefix before the master's registerAgentDevice.

Now: an agent_hdkd session (device_pubkey present) must pass the SAME on-chain SidecarRegistry.getDevice check the cap-mint path uses — registered_at!=0, !revoked, and device.actor_omni == session omni — or it gets 403 (audited as AuthFailed). Wallet/master sessions (no device_pubkey) are unaffected. cap.rs ChainContracts/DeviceEntry/call_get_device exposed pub(crate) for reuse. cargo test -p agentkeys-broker-server green. Refs #149.
…ly file (Codex finding #2)

The daemon's --init-link-code printed session_jwt in its stdout artifact; the harness captured it on the master and passed it to the sandbox MCP as --agent-session-bearer (a CLI arg), exposing the bearer in the master shell + the sandbox process list (ps), readable by co-resident untrusted code.

Now: the daemon writes J1_agent to an owner-only ~/.agentkeys/agent-session.jwt (0600) and emits the PUBLIC session_file path instead of the JWT. The MCP server reads the bearer from --agent-session-bearer-file (env MCP_AGENT_SESSION_BEARER_FILE); a direct --agent-session-bearer still wins when set. The harness (fresh-pairing) passes the file path, never the value; --reuse-agent keeps the value path. Bearer now travels daemon->file->MCP entirely in the sandbox. K10 private-key custody was already intact; this closes the derived-bearer leak. cargo build (broker+daemon+mcp) green; bash -n clean. Refs #149.
…CTOR_OMNI for cap-mint

Two issues surfaced on a full §10.2 fresh-pairing run:

P.2 false-FAIL (my finding-3 regression): the check ran 'jq -e' on $reg = heima-agent-create.sh 2>&1 (stderr logs + the JSON line). jq can't parse the mixed text → silently fails → P.2 reported FAIL even though registerAgentDevice SUCCEEDED (real tx 0x90f7…, block 9690030). Now extract the JSON line (grep -oE '{.*}' | tail -1) before jq.

cap-mint 400 'actor_omni must start with 0x' (1.5/3.1/4.2): the §10.2 child omni is un-prefixed by design (child_omni_hex), but cap-mint's validate_hex32 requires 0x (like OPERATOR_OMNI, which is already 0x). ACTOR_OMNI was set to the bare child omni → cap_mint rejected it. Now ACTOR_OMNI="0x${ds_actor#0x}" (exactly one 0x); ds_actor stays un-prefixed for the chain helpers + the child_omni== check.

Follow-up (not demo-blocking): a production agent whose session omni is un-prefixed hits the same cap-mint 400; the durable fix is cap-mint accepting un-prefixed omnis (validate_hex32 -> normalize_hex32) or the MCP normalizing the request. Tracked separately. Refs #149.
…hardened key writes, reuse bearer custody

A [high] oidc agent gate (oidc.rs) only checked active+actor; now mirrors the FULL cap-mint invariant — device.operator_omni == session parent_omni AND actor_omni == omni_account AND roles & ROLE_CAP_MINT AND active. Without the operator+role checks, any other registered operator could bind (this device hash, this actor) and the agent would pass, bypassing the master that issued the link code. Exposed cap.rs DeviceEntry.operator_omni/roles + ROLE_CAP_MINT as pub(crate).

B [high] write_key_0600 (agentkeys-core/device_crypto.rs): mode(0o600) only applies on CREATE, so a pre-existing loose-perms file kept its mode, and open() followed symlinks. Now rejects a pre-existing symlink/non-regular target and force-chmods 0600 after open. Residual TOCTOU (needs O_NOFOLLOW/libc) noted as follow-up.

C [medium] harness --reuse-agent passed --agent-session-bearer <value> (in the MCP argv/ps); now stages the master-minted bearer into the same owner-only sandbox file (umask 077) and passes only --agent-session-bearer-file, matching fresh-pairing. Neither mode leaves the JWT in ps.

cargo test -p agentkeys-core green (141+3); broker+daemon+mcp build clean; bash -n harness clean. Refs #149.
The agent-gate edit in d7a4c01 wasn't rustfmt-clean → CI 'cargo fmt --all -- --check' failed at 23s (parent_omni method chain + dkh map_err block). Ran cargo fmt --all (only oidc.rs affected). Verified locally with the EXACT CI commands: fmt --all --check clean, 'clippy --workspace --all-targets -- -D warnings' exit 0, 'test --workspace --test-threads=1' exit 0. Refs #149.
@hanwencheng hanwencheng merged commit dc94fba into main May 31, 2026
7 checks passed
hanwencheng added a commit that referenced this pull request May 31, 2026
Ports the Claude Design "agentkeyweb" handoff into apps/parent-control as the
primary, demoable operator experience. Maps 1:1 to the 9 user workflows.
Plan + verification + pushback: docs/plan/web-flow/issue-9step-flow.md.

Flow (workflow → component):
 1. WebAuthn login + onboarding ceremony  → ceremony.tsx (OnboardingScreen + CeremonyRunner)
 2. Memory panel: plant preserved memory  → memory.tsx (empty-state + plant ceremony,
    auto-detect existing, dedup guard — plant hidden + blocked once planted)
 3-4. Agent connects + master notified    → App bell + pairing request (post-#149: a pending binding)
 5. Request detail (agent + permissions)  → pairing.tsx request card
 6-7. Accept + Touch ID + ceremony        → WebAuthnModal → CeremonyRunner (PAIRING_STEPS)
 8. Device view + permission view         → pairing.tsx (device-grid) + permissions.tsx
    (mobile-style scoped PermissionList — replaces tables, the "won't scale" ask)
 9. Audit + decodable Heima TXs           → dashboard.tsx AuditFeed → EventDecodeModal
    (decodeCalldata mock; real decode tracked in #153)

New files:
 - lib/demoData.ts            seed actors/events + ONBOARDING_STEPS, PAIRING_STEPS,
                              PRESERVED_MEMORY, INCOMING_PAIRING, CHAIN_PROFILE,
                              VAULT_ITEMS, txHash, decodeCalldata (mock), ONCHAIN_KINDS
 - _components/ceremony.tsx   CeremonyRunner (progress bar + live step log + tx hashes) + OnboardingScreen
 - _components/memory.tsx     MemoryPage (plant / dedup / per-namespace listing)
 - _components/pairing.tsx    PairingPage (request → accept → device/permission view toggle)
 - _components/permissions.tsx PermSeg/PermSwitch/PermissionList/PermissionView (mobile scoped)
 - _components/dashboard.tsx  ActorsList, ActorDetail (editable PermissionList), AuditFeed

Changed:
 - _components/App.tsx        rewritten as the self-contained flow orchestrator:
                              onboarding gate (localStorage), header bell + badge,
                              memory/pairing/audit/chain routes, WebAuthn + pairing-ceremony
                              + tx-decode + memory-view modals
 - _components/types.ts       + CeremonyStep/PreservedMemory/PairingRequest/ChainProfile/
                              ContractInfo/RequestedPerm; Actor.justPaired; ChipKind +scope/device/k11;
                              Route +memory/pairing/chain
 - lib/constants.ts           CHIP_STYLES covers the new chip kinds
 - app/globals.css            ceremony/onboard/empty-memory/pair-req/view-toggle/device-grid/
                              bell/tx-decode/mem-body/perm-* blocks (no rounded corners, hairline rules)

Scope note: M1 visible flow is seed-data + local ceremony state (the prototype's model).
Real-daemon wiring stays behind the lib/client seam and is Phase 2 — #149 endpoints
(agent create / pending-bindings / bind / grant), onboarding + master-memory endpoints,
and the #153 audit decoder. The old client-based pages (pages.tsx/workers.tsx/onboarding.tsx)
remain on disk for that wiring; App no longer imports them.

Verified: npx tsc --noEmit clean · npm run build ok (4 static pages, 20.1 kB route) ·
dev smoke serves the onboarding screen (HTTP 200, all markers present).
hanwencheng added a commit that referenced this pull request Jun 1, 2026
… lowercase) (#154)

The image job tagged ghcr.io/${{ github.repository }}/agentkeys-mcp-server, but github.repository keeps the repo's real casing (litentry/agentKeys — capital K), so docker buildx rejected it: 'invalid tag ... repository name must be lowercase'. The job runs only on push to main (skips on PRs), so it first failed on the #149 merge (Actions run 26719167517).

Fix: a 'Resolve lowercase image name' step lowercases $GITHUB_REPOSITORY via tr (portable to bash 3.2 per CLAUDE.md, not the bash-4 ${,,}) → ghcr.io/litentry/agentkeys/agentkeys-mcp-server, fed to both :latest and :$sha tags. YAML validated; tr output verified locally.
hanwencheng added a commit that referenced this pull request Jun 1, 2026
…r-issued link codes (#159)

* plan: method-A agent-initiated pairing design (replaces #149 front-half)

Flip §10.2 from master-mints-link-code → agent-submits-request + master-claims-by-code (the IoT scan-the-device-QR model). Reuses #149's on-chain bind+scope tail. Unbind/factory-reset deferred → #156 (client) + #155 (on-chain self-revoke).

* agentkeys: §10.2 method-A pairing — broker request/claim/poll endpoints

Flip the agent bootstrap from master-initiated (link code) to agent-
initiated (the agent shows a code, the master claims it — the Matter/
HomeKit IoT model). Replaces #149's master-mint front-half; reuses the
on-chain bind + scope tail unchanged.

Broker:
- NEW storage/pairing_requests.rs — unbound, agent-created request pool
  (issue/claim/poll/pending_bindings/mark_bound/purge). J1 is NOT stored
  at rest; minted fresh at poll time on a re-proved pop_sig.
- NEW handlers/agent/request.rs (agent, pop_sig-gated) — open an unbound
  request, return {request_id (secret), pairing_code (display)}.
- NEW handlers/agent/claim.rs (master, J1-gated) — claim by code, derive
  O_agent=HDKD(O_master,//label), record pending binding.
- NEW handlers/agent/poll.rs (agent, pop_sig-gated) — once claimed, mint
  + return J1_agent.
- REMOVE handlers/agent/{create,redeem}.rs + storage/link_codes.rs.
- Rename link_code_store -> pairing_request_store across state/boot/main.
- Rewire routes: /v1/agent/pairing/{request,claim,poll}; keep
  pending-bindings + /ack (now keyed by request_id).

Tests: 14 store unit tests + agent_bootstrap_flow rewritten for the
request->claim->poll flow (5 cases incl. cross-device/bad-pop_sig poll
rejection). clippy --all-features --all-targets -D warnings clean.

Unbind/factory-reset re-pair deferred -> #156; on-chain self-revoke -> #155.

* agentkeys: §10.2 method-A pairing — daemon + CLI + harness + broker-host smoke

Flip the client + wire harness to agent-initiated pairing (issue #144,
method A), matching the broker request/claim/poll endpoints.

Daemon (--init-link-code → two one-shots mirroring the two endpoints):
- --request-pairing: in-sandbox K10 keygen → POST /v1/agent/pairing/request
  → print {request_id, pairing_code, …}; persist a 0600 state file so
  --retrieve-pairing can resolve request_id (--request-id overrides).
- --retrieve-pairing: poll /v1/agent/pairing/poll until claimed (bounded by
  --init-poll-timeout-seconds), mint+persist J1_agent (0600), emit artifact.

CLI: agent create → agent claim --pairing-code <code> --label … --services …
(POST /v1/agent/pairing/claim). agent pending unchanged (rows now keyed by
request_id).

Harness phase1-wire-demo.sh Phase P inverts: P.0 agent --request-pairing
(shows code) → P.1 master agent claim → P.1b agent --retrieve-pairing (J1) →
P.1c pending → P.2 bind + ack-by-request_id → P.3 grant. P.depair unchanged.
404 trap + route names updated.

setup-broker-host.sh (runbook-fix-fold-back): nm symbol grep →
pairing_{request,claim,poll}|pending_bindings; route smoke → no-bearer POST
/v1/agent/pairing/claim must be 401 not 404.

cli+daemon: clippy --all-targets -D warnings clean, fmt clean, tests pass
(38/38 single-threaded; the 1 parallel-suite failure is a pre-existing k11
enroll test race on a shared HOME path, unrelated). bash -n both scripts OK.

* agentkeys: §10.2 method-A pairing — docs + terminology (arch.md, runbooks)

Reconcile every doc + code-comment surface with the agent-initiated
pairing flip (issue #144, method A).

arch.md (single source of truth):
- §10.2 ceremony fully rewritten for method A (agent requests → master
  claims → agent retrieves), incl. the IoT/Sybil-safety rationale + the
  deferred unbind notes (#155/#156).
- §6.2 route list: /v1/agent/pairing/{request,claim,poll} replace
  create + link-code/redeem; pending-bindings ack now by request_id.
- §10.4 re-bootstrap inverted; §5 agent_omni row, §10.6 threat row,
  trust-boundary + actor-role tables, CLI inventory → pairing terms.
  Solidity link_code_redemption calldata param kept (contract unchanged).

operator-runbook-wire.md: Phase P walkthrough (P.0 request → P.1 claim →
P.1b retrieve → P.1c pending → P.2 bind+ack → P.3 grant), 404 trap +
route checks, troubleshooting rows.

v2-stage1-migration-and-demo.md §7: rewritten for method A (also fixes
pre-existing drift — the master, not the broker, submits registerAgentDevice).

issue-144 plan: superseded-front-half banner → method-A doc. issue-74
ephemeral-rebootstrap paragraph corrected. Code doc-comments (mcp-server
config, core actor_omni, cli device_session) → pairing terms.

fmt + clippy --all-targets -D warnings clean on the 3 comment-edited crates.

* agentkeys: method-A plan doc — mark implemented (unbind deferred #155/#156)

* agentkeys: setup-broker-host --ref uses git checkout -f (survive shadowing untracked files)

The --ref deploy path ran a plain `git checkout $PULL_REF`, which ABORTS
when an untracked working-tree file shadows a file the target ref tracks
("untracked working tree files would be overwritten by checkout" — hit on
the broker host when switching to a branch that tracks docs/wiki/*.md the
prior branch did not). The broker host is a deploy target, not a dev
checkout: -f overwrites the colliding files with the tracked version +
discards local edits to TRACKED files, while LEAVING unrelated untracked
files (env, keys, certs — all gitignored) intact. Folds the fix back into
the deploy runbook so the next operator does not hit the same abort.
hanwencheng added a commit that referenced this pull request Jun 1, 2026
* m1: parent-control web UI (closes #110)

Next.js 14 app under apps/parent-control/ implementing the Phase 1
mobile-responsive parent dashboard for the M1 demo.

Six pages, iii.dev-styled (IBM Plex Mono + Serif, cream/ink palette,
hairline rules, per-section accent hues):

- actors        — HDKD tree + devices/agents table + stats strip
- actor detail  — per-namespace scope toggles (deny/read/read+write),
                  payment-cap inputs, live cap-tokens with per-cap revoke
- audit feed    — SSE-simulated stream filterable by worker
- anchor status — countdown to next tier-2 batch + recent Merkle roots
- workers       — five worker cards (memory/credentials/audit/email/payment)
                  with per-actor usage share + trust profile
- logo          — six Bedlington Terrier variants for brand exploration

Demo Act 3 path is wired end-to-end: revoke device → K11 WebAuthn modal
with intent context (per arch.md §10.1) and mock Touch ID scan → on
confirm, actor flips to revoked status and a device.revoked event
appears at the top of the audit feed within ~200ms.

Stack matches issue #110: Next.js + thin client (no backend in this
project). Mock data is inlined for M1; M2 wires to the broker session
JWT + audit-service SSE feed (per #109).

Port 3113 aligns with arch.md §22c.1 (canonical web-UI surface). When
this UI is later folded into agentkeys daemon's `web` subcommand, the
URL stays identical.

Source: design handoff from claude.ai/design — port preserves visuals
1:1 while splitting the single-file React+Babel prototype into typed
TSX modules (types/data/shared/pages/workers/logos/App).

* parent-control: extract mocks, empty states, coverage scaffold (PR-A)

Foundation for issue #110 follow-up. Removes all inline mock data from
the parent-control UI and introduces a single AgentKeysClient interface
that every read + write call now flows through. Adds cargo-llvm-cov to
CI as a non-blocking artifact (threshold gating arrives in PR-C).

# What changed

apps/parent-control/lib/client/types.ts
  AgentKeysClient interface: listActors, getActor, listCapTokens,
  listRecentAuditEvents, streamAudit, listWorkers, getWorker,
  getAnchorStatus, updateScope, updatePaymentCap, revokeDevice,
  revokeCap, enrollK11Begin, enrollK11Finish. Discriminated Result<T>
  forces every consumer to handle the disconnected variant explicitly.

apps/parent-control/lib/client/empty.ts
  EmptyBackend — default implementation. Every method returns
  { ok: false, status: { kind: 'disconnected', reason: 'no-backend-configured' } }.
  No mock data. Operator sees explicit empty states.

apps/parent-control/lib/client/index.ts
  selectBackend() factory. Reads NEXT_PUBLIC_AGENTKEYS_BACKEND;
  defaults to 'empty'. 'daemon' falls back with a console warning
  until DaemonBackend lands in PR-C.

apps/parent-control/lib/ClientProvider.tsx
  React context + useClient() / useConnectionStatus() hooks.
  Wraps the whole app in app/layout.tsx.

apps/parent-control/lib/constants.ts
  NAMESPACES, CHIP_STYLES (config, not mock data).

apps/parent-control/app/_components/data.ts
  DELETED. Was the home of INITIAL_ACTORS, INITIAL_EVENTS, SIM_EVENTS.

apps/parent-control/app/_components/App.tsx
  Rewritten to fetch via useClient() on mount. Subscribes to
  client.streamAudit. Revoke flows now call client.revokeDevice +
  client.revokeCap; scope/payment updates call client.updateScope +
  client.updatePaymentCap with optimistic rollback on rejection.
  New sidebar section 'onboarding' with two stub pages (full wizard +
  WebAuthn ceremony land in PR-B).

apps/parent-control/app/_components/pages.tsx
apps/parent-control/app/_components/workers.tsx
  Empty-state rendering everywhere a list was previously inlined.
  ActorsPage, AuditPage take ConnectionStatus prop; WorkersPage owns
  its own fetch via useClient(). Every empty state explains what
  daemon endpoint will populate it.

apps/parent-control/app/_components/shared.tsx
  Adds <EmptyState status={...}> component used by every list page.

.github/workflows/coverage.yml
  cargo-llvm-cov via taiki-e/install-action. Runs on every PR that
  touches crates/**, generates lcov + html, attaches both as
  artifacts, prints summary to job summary. Non-blocking. Threshold
  gating lands in PR-C.

# Verified

- npm run typecheck — clean
- npm run build — 4 static pages, 16.5 kB route, 104 kB First Load JS
- npm run dev   — HTTP 200, empty state renders 'no actors enrolled'
                   + 'No daemon backend configured.' + harness hint;
                   no 'Sara' / 'FoloToy' / mock data in the SSR HTML.

# What did NOT land (intentional, per PR-A scope)

- DaemonBackend implementation (PR-C)
- Real WebAuthn ceremony (PR-B)
- Coverage threshold gate (PR-C)
- Harness v2-stage1 onboarding wizard (PR-B)
- Daemon HTTP endpoints for actors/audit/anchor/workers (PR-C)

* parent-control: real WebAuthn onboarding wizard (PR-B)

Issue #110 follow-up. Replaces the simulated 'Touch ID scan' modal
with a real browser-driven K11 WebAuthn ceremony backed by a new
daemon mode and HTTP surface.

# Daemon — new ui-bridge mode

crates/agentkeys-daemon/src/ui_bridge.rs (new)
  Dedicated HTTP surface for the parent-control web UI. Binds
  127.0.0.1:3114 by default, CORS-allows http://localhost:3113.
  Routes:
    GET  /healthz
    POST /v1/k11/enroll/begin   → returns PublicKeyCredentialCreationOptions
    POST /v1/k11/enroll/finish  → verifies attestation with webauthn-rs,
                                  returns credentialId + chain stub
  State is in-memory (pending HashMap keyed by user_id). On-chain
  SidecarRegistry.register_master_device() submission stubbed for M1
  (chain_tx_hash returns null); lands in PR-C.

crates/agentkeys-daemon/src/main.rs
  New --ui-bridge mode + 4 args (--ui-bridge-bind / --ui-bridge-origin /
  --ui-bridge-rp-id / --ui-bridge-rp-name). Independent of --proxy and
  --master-companion.

crates/agentkeys-daemon/Cargo.toml
  Adds webauthn-rs 0.5, tower-http 0.5 (cors feature), url 2.

# Daemon — unit tests (cargo llvm-cov visible)

crates/agentkeys-daemon/src/ui_bridge.rs::tests (6 tests, all green)
  - begin_returns_user_id_and_creation_options
  - begin_rejects_empty_username
  - finish_with_unknown_user_id_returns_no_pending
  - finish_with_malformed_credential_returns_malformed
  - replay_after_consume_returns_no_pending (verifies pending entry is
    only consumed once the credential parses; parse-stage failure leaves
    pending intact so the user can retry)
  - healthz_returns_ok

  Run: cargo test -p agentkeys-daemon --bin agentkeys-daemon ui_bridge

# UI — DaemonBackend

apps/parent-control/lib/client/daemon.ts (new)
  DaemonBackend implements AgentKeysClient. status() pings /healthz.
  enrollK11Begin / enrollK11Finish wire to the new daemon endpoints.
  All other methods return a 'not yet wired' disconnected variant
  until PR-C lands the read endpoints (actors, audit-SSE, anchor,
  workers).

apps/parent-control/lib/client/index.ts
  selectBackend() now actually constructs DaemonBackend when
  NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon.

# UI — real browser WebAuthn

apps/parent-control/lib/webauthn.ts (new)
  Helpers: base64url encode/decode, jsonToCreationOptions (server
  options → navigator.credentials.create() args), credentialToFinishPayload
  (PublicKeyCredential → daemon /finish JSON), webauthnAvailable +
  platformAuthenticatorAvailable feature detection.

apps/parent-control/app/_components/onboarding.tsx (new)
  Onboarding wizard mirroring harness/v2-stage1-demo.sh as 8 numbered
  steps. Step 3 (K11 WebAuthn) is LIVE — clicks 'run' invoke real
  navigator.credentials.create() via daemon /v1/k11/enroll/begin and
  ship the attestation to /v1/k11/enroll/finish. Other 7 steps are
  honestly labeled 'stubbed; lands in PR-C'.

apps/parent-control/app/_components/App.tsx
  Routes /onboarding to the live OnboardingPage (replaces the PR-A
  stub list).

# To exercise the real ceremony

  $ cargo run -p agentkeys-daemon -- --ui-bridge &
  $ cd apps/parent-control
  $ echo 'NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon' > .env.local
  $ npm run dev
  # open http://localhost:3113 → 'add device' → step 3 'run'
  # browser triggers Touch ID / Windows Hello / passkey UI for real

# Verified

- cargo build -p agentkeys-daemon — clean
- cargo test  -p agentkeys-daemon --bin agentkeys-daemon ui_bridge — 6/6 green
- npx tsc --noEmit                — clean
- npm run build                   — 4 static pages, 19.4 kB route, 107 kB First Load

# What did NOT land (intentional, per PR-B scope)

- Daemon read endpoints (/v1/actors, /v1/audit/stream, etc.)  → PR-C
- Identity ceremony, K10 gen, SIWE, STS, provision, chain bring-up,
  on-chain register-master-device wiring                      → PR-C
- Coverage threshold gate (blocking)                          → PR-C

* parent-control: full daemon read/write surface + harness mirror (PR-C)

Issue #110 follow-up. Wires the parent-control UI to a real daemon
backend end-to-end: read endpoints for actors / audit-SSE / anchor /
workers, write endpoints for scope / payment-cap / device-revoke /
cap-revoke, and a harness page mirroring the v2-stage2 + v2-stage3
shell scripts.

# Daemon — ui-bridge expansion

crates/agentkeys-daemon/src/ui_bridge.rs
  New ApiActor / ApiAuditEvent / ApiCapToken / ApiWorker / ApiAnchorStatus
  serializable types. New state: actors HashMap, caps HashMap,
  audit VecDeque (ring buffer, AUDIT_BUFFER_CAP=200), audit_tx
  broadcast::Sender for SSE, workers HashMap, anchor RwLock.

  New routes:
    GET  /v1/actors                       list_actors (sorted master-first)
    GET  /v1/actors/:id                   get_actor
    GET  /v1/actors/:id/caps              list_caps
    POST /v1/actors/:id/scope             update_scope + audit emit
    POST /v1/actors/:id/payment-cap       update_payment_cap + audit emit
    POST /v1/actors/:id/revoke            revoke_device + audit emit + cap clear
    POST /v1/actors/:id/caps/revoke       revoke_cap + audit emit
    GET  /v1/audit/recent?actor_id&limit  list_recent_audit (filterable)
    GET  /v1/audit/stream                 audit_stream (SSE via tokio broadcast)
    GET  /v1/anchor/status                anchor_status (dynamic next_anchor_in)
    GET  /v1/workers                      list_workers
    GET  /v1/workers/:id                  get_worker
    POST /v1/dev/seed                     dev_seed (operator-only data injection)
    POST /v1/dev/event                    dev_emit_event (manual audit emit)

  push_audit() helper ring-buffers + broadcasts in one place.

crates/agentkeys-daemon/Cargo.toml
  Adds futures-util 0.3 + tokio-stream 0.1 (sync feature) for SSE
  stream wrapping of the broadcast receiver.

# Daemon — tests (20 total, all green; previous 6 plus 14 new)

  list_actors_returns_empty_when_nothing_registered
  list_actors_returns_master_first
  get_actor_unknown_returns_404
  get_actor_known_returns_payload
  update_scope_writes_and_emits_audit
  update_scope_unknown_actor_404
  update_payment_cap_writes_and_emits_audit
  revoke_device_flips_status_and_clears_caps
  revoke_cap_removes_only_matching_cap_and_emits_audit
  dev_seed_populates_all_collections
  list_workers_empty_by_default
  get_worker_unknown_returns_404
  audit_buffer_caps_at_buffer_cap
  audit_stream_subscribes_before_emit_and_receives

  Run: cargo test -p agentkeys-daemon --bin agentkeys-daemon ui_bridge

# UI — DaemonBackend full wiring

apps/parent-control/lib/client/daemon.ts
  Every AgentKeysClient method now hits a real daemon endpoint:
  listActors, getActor (404 → null), listCapTokens, listRecentAuditEvents,
  streamAudit (EventSource on /v1/audit/stream listening for 'audit'
  events), listWorkers, getWorker, getAnchorStatus, updateScope,
  updatePaymentCap, revokeDevice, revokeCap, enrollK11Begin, enrollK11Finish.

  Wire-type translation (snake_case daemon JSON ↔ camelCase UI types)
  lives in apiToActor / apiToAuditEvent / apiToWorker helpers.
  normalizeStatus + normalizeChip clamp daemon strings to the UI's
  StatusKind + ChipKind unions.

# UI — harness mirror

apps/parent-control/app/_components/harness.tsx (new)
  New /harness route. Lists every step of v2-stage2-demo.sh (8 steps)
  and v2-stage3-demo.sh (15 steps) with file:line source pointers and
  the invariant each step protects (when applicable). Includes the
  operator runbook (`AGENTKEYS_CHAIN=heima bash harness/v2-stage{1,2,3}-demo.sh`).

apps/parent-control/app/_components/App.tsx
  Sidebar gains 'stage 2 + 3' under 'onboarding'. Routes /harness to
  HarnessPage. Adds 'harness' to the data-section accent set.

# CI — coverage gate now blocking

.github/workflows/coverage.yml
  Removes continue-on-error: true. Adds
  `cargo llvm-cov report --workspace --fail-under-lines 60`.
  60% is a conservative floor — the new ui_bridge.rs module is well
  above it (20 unit tests covering every handler) so it carries the
  workspace. Bump in follow-up PRs as other crates' coverage catches up.

# Verified

- cargo build -p agentkeys-daemon                — clean
- cargo test  -p agentkeys-daemon ui_bridge      — 20/20 green
- npx tsc --noEmit (apps/parent-control)         — clean
- npm run build                                  — 4 static pages, 19.6 kB route, 110 kB First Load

# To exercise end-to-end

  $ cargo run -p agentkeys-daemon -- --ui-bridge &
  $ curl -X POST http://localhost:3114/v1/dev/seed \
      -d @docs/dev-fixtures/parent-control-seed.json   # (operator can author)
  $ cd apps/parent-control
  $ echo 'NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon' > .env.local
  $ npm run dev
  # browse http://localhost:3113 — actors, audit-stream, revoke flows
  # are all live; no mock data anywhere in the codebase

# What did NOT land (called out explicitly per CLAUDE.md plan-completion-policy)

- Daemon-side wiring of stage-2 + stage-3 harness steps into a live
  status feed (clickable 'run' per step) — the harness page is a
  read-only mirror today. Live execution from the UI is a follow-up.
- On-chain SidecarRegistry.register_master_device() submission from
  the K11 enroll/finish handler — still stubbed (chain_tx_hash=null).
- Mobile-device cross-device WebAuthn (M5).
- Coverage threshold above 60% — bump once non-daemon crates add tests.

* plan: docs/plan/web-flow — parent-control operator user flow (Phase 1)

Plan-only commit. No implementation. Maps every harness v2-stage{1,2,3}
step into a natural operator user flow with real inputs, and locks the
Phase 1 scope to overview-Act-1 steps 1–7 (identity → cloud → chain
master register). Everything past step 7 is in an explicit TODO list.

# What's here

docs/plan/web-flow/README.md
  Index + how to read.

docs/plan/web-flow/overview.md
  End-to-end narrative · 4-screen Phase 1 state machine sketch ·
  Phase 1 endpoint inventory (12 new + 3 shipped) · TODO list for
  deferred work.

docs/plan/web-flow/stage1-first-run.md
  Harness v2-stage1 steps 6–11 → 4 UI screens A–D.
  Includes "Part B" on screen C: master vault + memory listings
  (per user feedback — the operator's own slice of the cloud is
  visible immediately after provisioning succeeds, separate from
  any agent inbox).
  Screens E, F (first agent, done) explicitly deferred to Phase 2.

docs/plan/web-flow/stage2-second-master.md
  Harness v2-stage2 → 6 screens G–L (pair, companion enroll, confirm,
  quorum, recovery drill, done). Entire stage marked deferred (Phase 3).

docs/plan/web-flow/stage3-agent-usage.md
  Agent bootstrap paths · live ops dashboard · on-demand isolation
  health check (16-step v2-stage3 against operator's real cloud).
  Entire stage marked deferred.

docs/plan/web-flow/input-discipline.md
  Real / Derived / Auto-generated triage. §1 resolves the
  operator-login-email vs agent-inbox-sub-address distinction
  explicitly (operator types sara@example.com; agent inbox is
  derived agent-folotoy@bots.litentry.org, system-derived, never
  operator-typed; email-service worker per arch.md §15.4 routes
  the agent's mail without touching the operator's inbox).

docs/plan/web-flow/data-model.md
  Daemon HTTP contract. Every endpoint tagged shipped / Phase 1 /
  deferred. Phase 1 surface is exactly 12 new endpoints + 3 shipped;
  everything else is called out as deferred to Phase 2 or Phase 3.

docs/plan/web-flow/deferred-and-followups.md
  What stays shell-only · operator-power-user escape hatches ·
  6 open questions for review (Q3 cross-browser passkey is the only
  one that blocks Phase 1) · 7-phase implementation sequencing
  (~9 days estimated).

docs/plan/README.md
  Adds an "Active plans" section pointing at agentkeys-memory-design
  and web-flow/.

# Phase 1 endpoint inventory (the only new endpoints to build)

  GET   /v1/onboarding/state         — umbrella state machine
  POST  /v1/auth/email/start         — broker-proxy: email magic link
  POST  /v1/auth/email/verify        — broker-proxy: magic-token verify
  GET   /v1/auth/email/status        — polled by the original tab
  POST  /v1/onboarding/cloud/provision    — dispatches 6 existing scripts
  GET   /v1/onboarding/cloud/stream  (SSE)  — per-script progress
  POST  /v1/onboarding/cloud/smoke   — envelope round-trip
  GET   /v1/master/credentials       — metadata listing (no plaintext)
  GET   /v1/master/memory            — metadata listing (no plaintext)
  POST  /v1/onboarding/chain/deploy  — 4 contracts: deploy or detect
  POST  /v1/onboarding/chain/register-master  — register_master_device
  POST  /v1/k11/assert/begin         — uniform K11 mutation pattern
  POST  /v1/k11/assert/finish

Three shipped endpoints (PR-B) used by Phase 1 without changes:
  GET   /healthz
  POST  /v1/k11/enroll/{begin,finish}

# Ready for review

Verify:
- stage docs match the harness scripts (spot-check any step against
  harness/v2-stage1-demo.sh's `# ─── Step N` headers).
- the email distinction in input-discipline.md §1 is correct
  (arch.md §15.4 email worker routes the agent's mail).
- the data-model.md daemon contract doesn't require rewriting any
  PR-C endpoint — only net-new endpoints + tagging existing ones.

* parent-control: scripts/dev.sh — single-terminal dev stack

Runs agentkeys-daemon --ui-bridge + Next.js dev server in one terminal
with color-prefixed multiplexed logs. Replaces the manual two-terminal
setup ("start the daemon in tab A, npm run dev in tab B, env-var the
backend kind by hand") with one command.

# What it does

apps/parent-control/scripts/dev.sh
  - Bash 3.2 compatible (macOS default /bin/bash).
  - Kills stale processes on UI_PORT (3113) + DAEMON_PORT (3114).
  - Auto-rebuilds agentkeys-daemon iff any .rs source is newer than the
    debug binary (cargo build -p agentkeys-daemon).
  - Starts the daemon in --ui-bridge mode, streams its stdout/stderr
    through a magenta [daemon] prefix.
  - Waits up to 5s for GET /healthz before launching the UI; fails
    fast with a clear error if the daemon dies during startup.
  - Pre-sets NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon +
    NEXT_PUBLIC_AGENTKEYS_DAEMON_URL=http://127.0.0.1:3114 for the
    Next.js child so the UI talks to the real daemon out of the box.
  - Starts npx next dev, streams its output through a cyan [ui] prefix.
  - Polls both PIDs; when either exits, sends SIGTERM to the other.
    Ctrl-C cleanly tears down both via a single trap.
  - All script-side status lines wear a bold-yellow [dev] prefix.

apps/parent-control/package.json
  Adds `npm run dev:stack` → `bash scripts/dev.sh`. The plain
  `npm run dev` remains the UI-only EmptyBackend path.

apps/parent-control/README.md
  New "dev:stack" subsection documenting the color scheme, what the
  script does, and the env overrides (UI_PORT, DAEMON_PORT, etc.).

# Verified

$ UI_PORT=3115 DAEMON_PORT=3116 bash apps/parent-control/scripts/dev.sh
  [dev]    starting daemon on http://127.0.0.1:3116 (rp_id=localhost)
  [daemon] ui-bridge serving bind=127.0.0.1:3116 origin=http://localhost:3115
  [dev]    daemon ready.
  [dev]    starting Next.js dev server on http://localhost:3115
  [ui]     ▲ Next.js 14.2.34
  [ui]      ✓ Ready in 1498ms
  [ui]     GET / 200 in 1118ms                 ← HTML title: "agentKeys · parent control"
  curl http://127.0.0.1:3116/healthz → {"ok":true,"surface":"ui-bridge"}

Ctrl-C tears both down. Re-run on stale port detects + kills the
previous run's leftovers automatically.

# Compatibility note

Initial draft used `wait -n` which requires bash 4.3+; macOS ships
bash 3.2 by default. Replaced with a `kill -0` polling loop so the
script runs on `/bin/bash` everywhere.

* dev.sh: move single-terminal dev stack to repo root

Previously at apps/parent-control/scripts/dev.sh; now at the repo
root so the entry point is one path away on a fresh clone.

Invocation:
  bash dev.sh                       # from the repo root
  ./dev.sh                          # from the repo root, same
  cd apps/parent-control && npm run dev:stack   # via npm wrapper

apps/parent-control/package.json
  dev:stack now calls `bash ../../dev.sh`.

apps/parent-control/README.md
  Updated to reference the new root location with all three invocation
  forms documented.

dev.sh (moved + path fixes)
  REPO_ROOT now resolves from the script's own dirname (which is the
  repo root). APP_DIR = "$REPO_ROOT/apps/parent-control". Added a
  preflight check that fails fast with a clear error if the script is
  copied somewhere that isn't the agentkeys repo root.

Verified:
  $ UI_PORT=3115 DAEMON_PORT=3116 bash dev.sh
    [dev]    starting daemon on http://127.0.0.1:3116
    [daemon] ui-bridge serving bind=127.0.0.1:3116
    [dev]    daemon ready.
    [ui]      ✓ Ready in 1526ms
  curl http://127.0.0.1:3116/healthz → {"ok":true,"surface":"ui-bridge"}
  curl http://localhost:3115/        → 200, <title>agentKeys · parent control</title>

* dev.sh: harden free_port, add agentkeys-mcp-server to the stack

Two changes addressing direct operator feedback.

# 1. Harden free_port — fix "Address already in use" after kill

Before: free_port sent a single SIGTERM and slept 0.4 s — fast enough
to race the kernel's port release, especially when a previous dev.sh
run's children were still in TIME_WAIT or hadn't actually shut down.
A second dev.sh would crash on:

  [daemon] Error: ui-bridge: bind TCP 127.0.0.1:3114
  [daemon] Caused by: Address already in use (os error 48)

After: free_port now does graceful → forceful → verify:
  1. Send SIGTERM.
  2. Poll up to 3 s waiting for the pid to exit (`kill -0`).
  3. If still alive, send SIGKILL.
  4. Re-check the port with lsof; abort the script with a clear error
     if it's still occupied (so the operator can investigate manually
     rather than hit the same cryptic bind error).

# 2. Bring up agentkeys-mcp-server as part of the stack

A third process joins the dev stack, with its own line-prefix color:

  [daemon]  magenta   agentkeys-daemon --ui-bridge       (port 3114)
  [mcp]     green     agentkeys-mcp-server               (port 8088)
  [ui]      cyan      npx next dev                       (port 3113)
  [dev]     yellow    this script's own status lines

Defaults: `--backend in-memory` (zero external dependencies — the MCP
server auto-seeds the three-act demo fixtures per
crates/agentkeys-mcp-server/README.md). `--listen 127.0.0.1:8088`.
Overridable via MCP_PORT + MCP_BACKEND.

The Next.js child also receives NEXT_PUBLIC_AGENTKEYS_MCP_URL so the
UI can call the MCP server once the stage-3 §1 agent-bootstrap flow
lands (Phase 2 — see docs/plan/web-flow/stage3-agent-usage.md).

# Refactored

- build_daemon_if_needed → generic build_if_needed taking a binary
  path + cargo package name + watched crate dirs. Reused for both
  the daemon and the mcp-server.
- cleanup() now iterates over all three pids.
- the "wait for either to exit" poll loop now watches all three.

# Verified

$ UI_PORT=3115 DAEMON_PORT=3116 MCP_PORT=8089 bash dev.sh
  [dev]    building agentkeys-mcp-server (debug)...
  [dev]    starting daemon on http://127.0.0.1:3116
  [daemon] ui-bridge serving bind=127.0.0.1:3116
  [dev]    daemon ready.
  [dev]    starting mcp-server on http://127.0.0.1:8089 (backend=in-memory)
  [mcp]    agentkeys-mcp-server listening (HTTP)
  [dev]    mcp-server ready.
  [dev]    starting Next.js dev server on http://localhost:3115
  [ui]     Ready in 1.5 s

  curl http://127.0.0.1:3116/healthz → {"ok":true,"surface":"ui-bridge"}
  curl http://127.0.0.1:8089/        → 404 (server listening; bare / unmapped)
  curl http://localhost:3115/        → 200

* dev.sh: fix free_port multi-pid handling — make it truly idempotent

Operator hit:

  [dev] port :3114 held by pid 57667
  90638 — sending SIGTERM
  [dev] port :3114 is still occupied after SIGKILL — investigate manually
  [dev]   lsof -i tcp:3114

The first line reveals the bug: `lsof -ti tcp:3114` returns ONE pid
per line, but a process listening on both IPv4 and IPv6 (or with a
shared child) shows up as TWO pids. The previous code captured the
multiline string into one variable and then did:

  kill "$pid"   # $pid == "57667\n90638"

which is malformed. `kill` errors out silently (the `|| true` suppresses
it), so nothing dies. The verification re-checks lsof, sees the pids
still there, and aborts the script.

Fix: free_port now iterates over each pid individually for both SIGTERM
and SIGKILL. Added a second cleanup pass — if any new pid grabbed the
port between the kill and the check (rare but possible during daemon
restarts), the second pass kills it too. Only after the second pass
fails does free_port abort.

Verified:

$ ./target/debug/agentkeys-daemon --ui-bridge ... &  # plant squatter
$ lsof -ti tcp:3114
57667
90638                                                # two pids — reproduces bug
$ UI_PORT=3115 DAEMON_PORT=3114 bash dev.sh
  [dev] port :3114 held by pid 57667 — sending SIGTERM (pass 1)
  [dev] port :3114 held by pid 90638 — sending SIGTERM (pass 1)
  [daemon] ui-bridge serving bind=127.0.0.1:3114
  [dev] all three processes running. Ctrl-C to stop.
$ curl http://127.0.0.1:3114/healthz
{"ok":true,"surface":"ui-bridge"}                    # new daemon, not the squatter

The script is now idempotent against any number of stale processes
holding the dev ports (3113, 3114, 8088 — or whatever the operator
overrides via env). Re-running after a hard kill / lost terminal
cleans up the prior run and starts fresh.

* dev.sh: silent + clean shutdown — fix Ctrl-C hang, suppress noise

Operator hit:
  ^C
  [dev] shutting down…           ← trap fired, but then script hung
  (no further output, no prompt, ports still bound)

Plus a cosmetic glitch:
  [dev] \033[2magentkeys-daemon binary is current — skipping build

# Root causes + fixes

## 1. Literal "\033[2m" leaked into the build-skip line

The printf format used %s for $C_DIM. %s prints the literal string;
%b is what interprets backslash escapes. Single-quoted bash strings
don't process \033, so $C_DIM stays as the 6-char literal until %b
unfolds it. Fixed by reordering the format specifier.

## 2. Ctrl-C hung the script — process substitution kept fds open

Previous attempt used `> >(prefix ...)` so $! would resolve to the
real binary pid. That fixed pid tracking but introduced a subtler bug:
process substitution opens an fd in the parent shell pointed at the
reader's stdin. Even after the daemon binary exits, the script still
holds that fd open, so the prefix reader never sees EOF, and `wait`
blocks forever in cleanup.

Fix: switch to named FIFOs in a per-run temp dir
($TMPDIR/agentkeys-dev-stack-$$/). For each process:

  prefix "$C_X" "x" < "$FIFO_X" &       # reader, blocks on FIFO read
  PREFIX_X_PID=$!
  disown "$PREFIX_X_PID"
  "$X_BIN" ... > "$FIFO_X" 2>&1 &       # writer; $! = real binary pid
  X_PID=$!
  disown "$X_PID"

The script itself never opens the FIFO, so kill -> binary exit ->
writer fd closes -> reader sees EOF -> reader exits. Clean.

## 3. "Terminated: 15" job-control noise

When `wait` reaped the SIGTERM'd children, bash printed termination
notices ("dev.sh: line 198: 34855 Terminated: 15  $DAEMON_BIN ...").
These appeared between [dev] shutting down… and [dev] stopped.,
muddying the operator's view of what happened.

Fix: `disown` each backgrounded pid right after capture. Bash drops
the job from its job table, so SIGCHLD reaping is silent. Replaced
the cleanup's `wait` with a polling loop (`kill -0` in a tight loop)
since `wait` doesn't accept disowned pids.

## 4. False "one of the children exited" warning after clean shutdown

After cleanup() returned, control fell back to the polling-loop's
post-condition where `warn` printed about an unexpected child exit
— misleading after an operator-initiated shutdown.

Fix: `exit 0` at the end of cleanup() so the script terminates
immediately without re-entering the polling loop.

## 5. set +m

Added `set +m` at the top to disable job-control monitor mode. With
disown this is belt-and-braces, but it removes the last possible
source of "[N]+ Done" / "[N]+ Terminated" announcements.

# Verified

$ bash dev.sh   # then SIGTERM the script pid 1 s later
  ...
  [dev] all three processes running. Ctrl-C to stop.
  [ui]  ✓ Starting...
  ^TERM
  [dev] shutting down…
  [dev] stopped.

  'Terminated' hits in log:            0
  'dev.sh: line' hits in log:          0
  'one of the children' hits in log:   0
  pids on :3113, :3114, :8088:         empty after shutdown
  total shutdown duration:             1 second

* plan(web-flow): redesign agent flow around #141 wire/hook model

Merged origin/main (#140 IAM strategy reset + #141 agentkeys wire/hook +
#137 audit-vector exporter + #138 CI hardening). The merge brought in the
Authority-Host / Task-Host model, which obsoletes the prior agent-onboarding
design (paste-a-pair-code into a remote sandbox). Redesigned the web-flow plan
to match.

What changed in the plan:

- stage3-agent-usage.md — full rewrite. Agent onboarding is now pair (agentkeys
  agent device-session — key born in the runtime, never on the master) → wire
  (agentkeys wire installs 3 IAM-guarantee hooks the LLM can't bypass:
  pre_tool_call→check, post_tool_call→audit, pre_llm_call→memory-inject) → the
  three acts (permissioned memory / deterministic denial / audit) + the
  memory-aware surprise (deterministically backed by `hermes hooks test
  pre_llm_call`, not a chat reply). Adds the hook-aware live dashboard +
  guarantee-health panel. Preserves the 16-step isolation health check.

- overview.md — new "two-host model" section up top (Authority Host vs Task
  Host; IAM tool vs IAM guarantee). Act-3 TODO reframed as "Phase 2 — the wire
  flow." Master onboarding (Phase 1) unchanged.

- data-model.md — replaced the bootstrap/* endpoints with the pair/wire/observe
  surface: /v1/agents/pair/{init,bind,approve-scope}, /v1/agents/:id/{wire,
  unwire,verify/memory-inject,guarantee-health}, hook-tagged /v1/audit/stream.
  Notes the per-actor STS relay config + MCP port 18088.

- input-discipline.md — §2.5: runtime choice + wire namespaces/payment-scope are
  Real inputs; the agent device key is born in the runtime (master never holds
  it); the runtime list reflects real adapter support, never faked.

- README.md — redesign banner + updated source-of-truth + file-map row.

- dev.sh — MCP_PORT default 8088 → 18088 (8088 collides with the sandbox
  gem-server, per #141); header comments aligned.

Plan only — no implementation. Master-onboarding docs (stage1/stage2) untouched.

* parent-control: implement the 9-step operator flow (Claude-design port)

Ports the Claude Design "agentkeyweb" handoff into apps/parent-control as the
primary, demoable operator experience. Maps 1:1 to the 9 user workflows.
Plan + verification + pushback: docs/plan/web-flow/issue-9step-flow.md.

Flow (workflow → component):
 1. WebAuthn login + onboarding ceremony  → ceremony.tsx (OnboardingScreen + CeremonyRunner)
 2. Memory panel: plant preserved memory  → memory.tsx (empty-state + plant ceremony,
    auto-detect existing, dedup guard — plant hidden + blocked once planted)
 3-4. Agent connects + master notified    → App bell + pairing request (post-#149: a pending binding)
 5. Request detail (agent + permissions)  → pairing.tsx request card
 6-7. Accept + Touch ID + ceremony        → WebAuthnModal → CeremonyRunner (PAIRING_STEPS)
 8. Device view + permission view         → pairing.tsx (device-grid) + permissions.tsx
    (mobile-style scoped PermissionList — replaces tables, the "won't scale" ask)
 9. Audit + decodable Heima TXs           → dashboard.tsx AuditFeed → EventDecodeModal
    (decodeCalldata mock; real decode tracked in #153)

New files:
 - lib/demoData.ts            seed actors/events + ONBOARDING_STEPS, PAIRING_STEPS,
                              PRESERVED_MEMORY, INCOMING_PAIRING, CHAIN_PROFILE,
                              VAULT_ITEMS, txHash, decodeCalldata (mock), ONCHAIN_KINDS
 - _components/ceremony.tsx   CeremonyRunner (progress bar + live step log + tx hashes) + OnboardingScreen
 - _components/memory.tsx     MemoryPage (plant / dedup / per-namespace listing)
 - _components/pairing.tsx    PairingPage (request → accept → device/permission view toggle)
 - _components/permissions.tsx PermSeg/PermSwitch/PermissionList/PermissionView (mobile scoped)
 - _components/dashboard.tsx  ActorsList, ActorDetail (editable PermissionList), AuditFeed

Changed:
 - _components/App.tsx        rewritten as the self-contained flow orchestrator:
                              onboarding gate (localStorage), header bell + badge,
                              memory/pairing/audit/chain routes, WebAuthn + pairing-ceremony
                              + tx-decode + memory-view modals
 - _components/types.ts       + CeremonyStep/PreservedMemory/PairingRequest/ChainProfile/
                              ContractInfo/RequestedPerm; Actor.justPaired; ChipKind +scope/device/k11;
                              Route +memory/pairing/chain
 - lib/constants.ts           CHIP_STYLES covers the new chip kinds
 - app/globals.css            ceremony/onboard/empty-memory/pair-req/view-toggle/device-grid/
                              bell/tx-decode/mem-body/perm-* blocks (no rounded corners, hairline rules)

Scope note: M1 visible flow is seed-data + local ceremony state (the prototype's model).
Real-daemon wiring stays behind the lib/client seam and is Phase 2 — #149 endpoints
(agent create / pending-bindings / bind / grant), onboarding + master-memory endpoints,
and the #153 audit decoder. The old client-based pages (pages.tsx/workers.tsx/onboarding.tsx)
remain on disk for that wiring; App no longer imports them.

Verified: npx tsc --noEmit clean · npm run build ok (4 static pages, 20.1 kB route) ·
dev smoke serves the onboarding screen (HTTP 200, all markers present).

* parent-control: implement pushback #2 — real onboarding WebAuthn + real memory

After merging #159 (§10.2 agent-initiated pairing, method A — which resolves
pushback #1 upstream), wire the two genuinely-real pieces the user asked for.
Plan + status: docs/plan/web-flow/issue-9step-flow.md.

Daemon (ui_bridge.rs) — master memory, real + idempotent:
 - ApiMemoryEntry + master_memory store; content_hash = sha256(ns ‖ key ‖ body).
 - GET  /v1/master/memory          — list (sorted by ns/key).
 - POST /v1/master/memory/plant    — idempotent: dedup by content_hash →
                                     {planted, skipped, total}; emits a
                                     memory.write audit row when something lands.
 - dev_seed extended with master_memory (seeds the "already has memory" path).
 - 3 new unit tests (empty / plant→replant-dedup / changed-body-adds-entry);
   23 ui_bridge tests pass. Adds sha2 dep.

Client seam (lib/client) — new MasterMemoryEntry/PlantResult + listMasterMemory()
 + plantMemory(): DaemonBackend hits the real endpoints; EmptyBackend stays
 disconnected (offline → seed fallback in the UI).

UI:
 - OnboardingScreen (ceremony.tsx): real navigator.credentials.create() via the
   client (/v1/k11/enroll/{begin,finish}, PR-B) when a daemon is configured —
   shows a "K11 enrolled · real WebAuthn" chip; narrated fallback offline. No
   longer a pure setTimeout fake.
 - MemoryPage wiring (App.tsx): auto-detect existing memory on load
   (listMasterMemory → hides the plant button); plant calls plantMemory (server
   dedups) then re-lists; seed fallback when disconnected. The dedup guard is
   now enforced both client-side AND server-side.
 - pairing.tsx copy aligned to method A (agent shows a code → master claims it),
   matching #159. Functional claim-input + daemon pairing-proxy is the next step
   (needs the broker reachable).

Pushback status: #1 resolved by #159; #2 implemented here; #3 (audit decode)
remains a mock tracked in #153 (per the user's "just 2" scope).

Verified: cargo test -p agentkeys-daemon ui_bridge — 23/23 · npx tsc --noEmit clean ·
npm run build ok (21.2 kB route) · dev smoke renders onboarding, no runtime errors.

* parent-control: remove all mock/seed data — client-drive every page + email-first §9 onboarding

- demoData.ts: strip INITIAL_ACTORS / INITIAL_EVENTS / SIM_EVENTS / PRESERVED_MEMORY /
  INCOMING_PAIRING / VAULT_ITEMS / ONBOARDING_STEPS / MASTER_DEVICES. Keep ONLY the
  audit tx-decode mock (txHash / decodeCalldata / ONCHAIN_KINDS / contractFor, GH #153),
  PAIRING_STEPS narration, and CHAIN_PROFILE config (contract addresses now placeholder).
- App.tsx: actors / events / memory load from the lib/client seam
  (listActors / listRecentAuditEvents / streamAudit / listMasterMemory). Drop the synthetic
  SSE tick, the 9s INCOMING_PAIRING timer, pushEvent echoes, and the hardcoded Hermes actor.
  Revoke routes through client.revokeDevice; the pairing ceremony re-fetches the actor tree.
  Header identity + connection status derive from real data (no Sara / iPhone / fake block / ttl).
- dashboard / memory / permissions: render EmptyState when disconnected (the default
  EmptyBackend) and neutral empty copy when connected-but-empty. Vault items come via a prop
  (default []); the memory page is read-only (no fixture-plant button).
- ceremony.tsx + types.ts: first-run is the arch.md §9 master-bootstrap ceremony — real email
  FIRST, then K10 keygen -> email verify -> K11 Touch ID bound MID-ceremony (CeremonyStep.action)
  -> wallet + SIWE -> register_master_device. There is no separate 'register' step.

Verified: tsc --noEmit clean; next build clean (4/4 static pages).

* parent-control: plant the PREPARED real memory archive (rework, not fixture)

The memory plant button is reworked to import a PREPARED canonical archive through
the real client seam (plantMemory -> daemon POST /v1/master/memory/plant, content-hash
dedup) instead of the removed kevin.zhao display fixture.

- lib/preparedMemory.ts (NEW): PREPARED_MEMORY = the documented demo dataset the rest of
  the system already uses — the 'Chengdu trip' the wire demo seeds (SEED_MEMORY_CONTENT)
  plus the per-namespace composition from docs/agent-iam-strategy.md §3.5
  (travel / personal / family). Bytes computed from the body; sent without contentHash
  (the daemon computes + dedups server-side).
- memory.tsx: plant button + ceremony return, gated to connected + empty. Disconnected ->
  EmptyState (no daemon to plant into). Has-memory -> read-only per-namespace list.
- App.tsx: planting state + plantMemory/plantDone restored; plantDone calls
  client.plantMemory(PREPARED_MEMORY) -> re-lists from the daemon. On disconnected it
  toasts 'Connect a daemon to plant prepared memory' (no client-side faking).

Verified: tsc --noEmit clean; next build clean (4/4 static pages).

* parent-control: add log out button (header + sidebar) with full session reset

- logout() clears the ak_onboarded localStorage flag and resets all in-memory view
  state (actors / events / memory / planting / pairing / modals / nav) so the next
  login starts clean, returning to the §9 email/onboarding screen.
- Surfaced as a header 'log out' button next to the master identity, and the sidebar
  'account' nav item (was the buried 'replay onboarding' partial reset) now calls the
  same handler.

Verified: tsc --noEmit clean; next build clean (4/4 static pages).

* parent-control: remove superseded/unused frontend code

Drop dead component files left over from the 9-step-flow port (replaced by
dashboard.tsx + ceremony.tsx, never imported by App.tsx — the sole entry is
page.tsx → App):
- app/_components/pages.tsx        (→ dashboard.tsx)
- app/_components/onboarding.tsx   (→ ceremony.tsx OnboardingScreen)
- app/_components/harness.tsx      (unreferenced)
- app/_components/workers.tsx      (unreferenced)

Cascade cleanup of symbols they were the only consumers of:
- shared.tsx: remove TripleToggle (only used by pages.tsx) + AsciiRule (unused);
  drop now-unused ScopeBits import.
- types.ts: remove SimEvent (only fed the deleted SIM_EVENTS) + Route (unused).

Verified: tsc --noEmit clean; next build clean (4/4 static pages).

* agentkeys-daemon: rustfmt ui_bridge.rs + main.rs (fix cargo fmt --check CI)

The master-memory + ui-bridge test code (added earlier in this PR) was committed
without rustfmt, failing the 'cargo fmt + clippy + test' required check on #136.
cargo fmt --all is behavior-preserving (test bodies only); the companion
'test + clippy' job already passed on this commit.

* ci(coverage): fix cargo-llvm-cov report invocation (drop --workspace/test-args)

The non-blocking coverage job failed at the report step:
  error: --workspace is specific to [test,nextest,...] and not supported
  for subcommand 'report'
A newer cargo-llvm-cov rejects --workspace (and trailing -- <test-args>) on the
'report' subcommand. Collect once with 'cargo llvm-cov --no-report --workspace',
then emit lcov/html/summary + apply --fail-under-lines 60 via plain 'report'
calls. Same artifacts + gate; one test pass instead of four.

* agentkeys-daemon: fix 2 clippy lints in ui_bridge (clippy --all-targets -D warnings)

The 'cargo fmt + clippy + test' job runs 'cargo clippy --workspace --all-targets
-- -D warnings', which (unlike the other clippy job) lints test code and denies
warnings. Two pre-existing lints only surfaced once the fmt fix let clippy run:
- plant_master_memory: contains_key-then-insert → use the HashMap entry API
  (clippy::map_entry). Behavior-identical (vacant → insert+plant; occupied → skip).
- a test: assert_eq!(..., true) → assert!(...) (clippy::bool_assert_comparison).

Verified: cargo fmt --all --check clean; cargo clippy -p agentkeys-daemon
--all-targets -- -D warnings clean; cargo test -p agentkeys-daemon passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Full arch.md §10.2 agent-bootstrap ceremony (HDKD omni + broker link-code endpoints)

1 participant